/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.resources.impl.internal;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

import nemos.resources.IResources;
import nemos.resources.IResourcesManager;
import nemos.resources.UnsupportedResourceType;
import nemos.resources.impl.IControl;
import nemos.resources.impl.IExternalFolder;
import nemos.util.osgi.BundleClassLoader;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;

/**
 * ResourcesManagerImpl : The {@link IResourcesManager} standard implementation.
 * 
 * <p>
 * This implementation delagates the {@link IResources} generation to the
 * {@link IControl} services and the external File resolution to the
 * {@link IExternalFolder} services.
 * </p>
 * 
 * @see IControl
 * @see IExternalFolder
 * 
 * @author Romain DUBOIS
 */
@Reference(name = "folder", referenceInterface = IExternalFolder.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
@Service(IResourcesManager.class)
@Component(specVersion = "1.1", name = "nemos.resources")
public class ResourcesManagerImpl extends AbstractFileManager implements IResourcesManager {

    /** The fallback dir property */
    private static final String FALLBACK_DIR_PROPERTY = "nemos.resources.fallbackDir"; //$NON-NLS-1$
    /** The default fallback dir */
    private static final String DEFAULT_FALLBACK_DIR = "resources-default"; //$NON-NLS-1$
    /** The type filter pattenr */
    private static final String TYPE_FILTER = "(" + IControl.TYPE_PROPERTY + "=%s)"; //$NON-NLS-1$ //$NON-NLS-2$

    /** The bundle context */
    private BundleContext mContext;
    /** The Folder services map */
    private final Map<String, IExternalFolder> mFolders;
    /** The default folder used for types with no service */
    private File mDefaultFolder;

    /** The log service */
    @Reference(name = "log", cardinality = ReferenceCardinality.OPTIONAL_UNARY)
    private LogService mLogger;

    /**
     * Constructor
     */
    public ResourcesManagerImpl() {
        super();
        mFolders = new HashMap<String, IExternalFolder>();
        mDefaultFolder = new File(DEFAULT_FALLBACK_DIR);
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getExternalFile(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public File getExternalFile(final String pType, final String pName) {
        final IExternalFolder lFolder = mFolders.get(pType);

        File lFile;
        if (lFolder == null) {
            final File lDir = new File(mDefaultFolder, escape(pType));
            if (!lDir.exists()) {
                lDir.mkdirs();
            }
            lFile = new File(lDir, escape(pName));
        } else {
            lFile = lFolder.getExternalFile(pName);
        }

        return lFile;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getResources(java.lang.String,
     *      java.lang.Class)
     */
    @Override
    public <T> IResources<T> getResources(final String pBaseName, final Class<T> pType)
            throws UnsupportedResourceType {
        return getResources(pBaseName, pType, null);
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getResources(java.lang.String,
     *      java.lang.Class, java.util.Locale)
     */
    @Override
    public <T> IResources<T> getResources(final String pBaseName, final Class<T> pType,
            final Locale pLocale) throws UnsupportedResourceType {

        // Check parameters
        if (pType == null) {
            throw new UnsupportedResourceType(pType);
        }
        Locale lLocale = pLocale;
        if (lLocale == null) {
            lLocale = Locale.getDefault();
        }

        // Look for control
        IControl lControlSvc = null;
        ServiceReference<IControl> lRef = null;
        try {
            final Iterator<ServiceReference<IControl>> lRefsIt = mContext.getServiceReferences(
                    IControl.class, String.format(TYPE_FILTER, pType.getName())).iterator();

            while (lControlSvc == null && lRefsIt.hasNext()) {
                lRef = lRefsIt.next();
                lControlSvc = mContext.getService(lRef);
            }
        } catch (final InvalidSyntaxException e) {

            // Local copy in order to avoid synchronized bloc
            final LogService lLogger = mLogger;

            if (lLogger != null) {
                lLogger.log(LogService.LOG_ERROR, "Internal error", e); //$NON-NLS-1$
            }
        }

        try {
            // Build the real Control object
            Control lControl = null;
            if (lControlSvc == null) {
                lRef = null;

                // Try the default types
                if (pType == String.class) {
                    lControl = Control.getControl(Control.FORMAT_PROPERTIES);
                } else if (pType == Object.class) {
                    lControl = Control.getControl(Control.FORMAT_CLASS);
                } else {
                    throw new UnsupportedResourceType(pType);
                }
            }
            // Adapt the service
            else {
                lControl = new ControlAdapter(lControlSvc, pType);
            }

            // Rely on the java ResourceBundle mechanism
            try {
                final ResourceBundle lRsrcBundle = ResourceBundle.getBundle(pBaseName, lLocale,
                        new BundleClassLoader(mContext.getBundle()), lControl);

                // Adapt the result
                IResources<T> lResult = null;
                if (lRsrcBundle instanceof IResources) {
                    lResult = ((IResources<?>) lRsrcBundle).adapt(pType);
                } else if (lRsrcBundle != null) {
                    lResult = new ResourcesAdapter<T>(lRsrcBundle, pType);
                }
                return lResult;

            } catch (final MissingResourceException e) {
                return null;
            }
        } finally {
            if (lControlSvc != null) {
                mContext.ungetService(lRef);
            }
        }
    }

    /**
     * Start the component.
     * 
     * @param pContext
     *            The component context
     */
    @Activate
    protected void start(final BundleContext pContext) {
        mContext = pContext;
    }

    /**
     * Update the component properties.
     * 
     * @param pProperties
     *            The component properties
     */
    @Modified
    public void updated(final Map<String, Object> pProperties) {
        mDefaultFolder = checkOrCreateFolder(pProperties, FALLBACK_DIR_PROPERTY,
                DEFAULT_FALLBACK_DIR);
    }

    /**
     * Bind a new folder.
     * 
     * @param pFolder
     *            The folder to bind
     * @param pParams
     *            The folder service parameters
     */
    protected void bindFolder(final IExternalFolder pFolder, final Map<String, Object> pParams) {
        final Object lTypes = pParams.get(IExternalFolder.TYPE_PROPERTY);

        // Can be an array
        if (lTypes instanceof String[]) {
            synchronized (mFolders) {
                for (final String lType : (String[]) lTypes) {
                    mFolders.put(lType, pFolder);
                }
            }
        }
        // A collection
        else if (lTypes instanceof Collection<?>) {
            synchronized (mFolders) {
                for (final Object lType : (Collection<?>) lTypes) {
                    if (lType instanceof String) {
                        mFolders.put((String) lType, pFolder);
                    }
                }
            }
        }
        // Or a single one
        else if (lTypes instanceof String) {
            mFolders.put(lTypes.toString(), pFolder);
        }
    }

    /**
     * Unbind a folder.
     * 
     * @param pFolder
     *            The folder to unbind
     */
    protected void unbindFolder(final IExternalFolder pFolder) {
        synchronized (mFolders) {
            final Iterator<Entry<String, IExternalFolder>> lIt = mFolders.entrySet().iterator();
            while (lIt.hasNext()) {
                if (lIt.next().getValue().equals(pFolder)) {
                    lIt.remove();
                }
            }
        }
    }

    /**
     * Bind the Log service.
     * 
     * @param pLogService
     *            The log service
     */
    protected void bindLog(final LogService pLogService) {
        mLogger = pLogService;
    }

    /**
     * Unbind the Log service.
     * 
     * @param pLogService
     *            The log service
     */
    protected void unbindLog(@SuppressWarnings("unused") final LogService pLogService) {
        mLogger = null;
    }
}
